Add scripts to allow addons from personal repos to be synchronized with Crowdin#1
Add scripts to allow addons from personal repos to be synchronized with Crowdin#1nvdaes wants to merge 119 commits intonvaccess:masterfrom
Conversation
… addedwith dev role to Crowdin if they use a project not owned by them to upload source files)
…flow to upload/update files in Crowdin
…k pass creating a PR at nvdaes/translateNvdaaddonsWithCrowdin repo
| strictSetInference = true | ||
|
|
||
| # Compliant rules | ||
| reportAbstractUsage = true |
There was a problem hiding this comment.
it's probably better to keep these rules than dropping to NVDA's standard
PurposeAdd-on authors may wish to help translators use Crowdin, the same framework where they translate NVDA. to translate messages and documentation for maintained add-ons: Other details
Development approachA workflow (GitHub Actions), and several scripts (Python and Powershell), as well as a json file with language mappings have been added. |
|
I've tested that all check pass using this pyproject.toml file on this PR: nvdaes/translateNvdaAddonsWithCrowdin#11 I use precommit, CodeQL and a workflow to check that all translatable messages have comments for translators. I'll try to use the cache action to cache some add-on metadata like its id, and also hashfiles from l10nSources (taking the value of buildVars.py), and the hasf¡hfile of the readme.md, to determine if pot and xliff files should be updated. |
|
Export translations to Crowdin running the workflow with update=False works properly: https://github.com/nvdaes/translateNvdaAddonsWithCrowdin/actions/runs/19802210157 |
|
This time, updatexLiff is failing. Seems that adding blank lines to readme may cause problems: https://github.com/nvdaes/translateNvdaAddonsWithCrowdin/actions/runs/19802391926/job/56731562709 |
|
If someone can help with this issue when update xliff, I'll be grateful. |
|
It might be easier to avoid xliff and just translate the markdown files directly. This won't support diffs very well but worth experimenting with |
|
@seanbudd wrote:
OK. |
|
@CyrilleB79, you were interested in this framework. If you want, feel free to see how the translateNvdaAddonsWithCrowdin.md can be translated in the project. Using xliff files is causing problems, as mentioned, and we are experimenting uploading md files instead. |
|
Hi @nvdaes, That’s a great point. The However, you are right: if our |
|
Great @abdel792. If langCodes.py is not needed, let's remove it. I plan to
work on this in a few hours, if you cannot do it before.
El mié, 29 abr 2026 a las 10:29, abdel792 ***@***.***>)
escribió:
… *abdel792* left a comment (nvaccess/AddonTemplate#1)
<#1 (comment)>
Hi @nvdaes <https://github.com/nvdaes>,
That’s a great point. The langCodes.py script was originally included to
handle specific edge cases where Crowdin language codes and NVDA local
directory names might differ (such as handling underscores vs dashes, or
specific regional variants).
However, you are right: if our languageMappings.json and NVDA lang
directories correspond with _addonL10n subfolders structure, we could
definitely simplify this.
—
Reply to this email directly, view it on GitHub
<#1 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADYTVZB5R5EAXRWQ4VHQQXT4YG4M7AVCNFSM6AAAAACNAEHU72VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DGNBSGA2DGOJVG4>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
- Updated crowdinSync.ps1 to use $langCode directly for local paths, as it matches NVDA's structure[cite: 2]. - Removed the intermediate $localLangDir variable for better readability[cite: 2]. - Deleted the now obsolete langCodes.py script[cite: 2]. - Improved internal documentation regarding language code handling[cite: 2]. Special thanks to @nvdaes for suggesting this optimization!
|
Hi @nvdaes, I've just implemented the simplification you suggested! Since the folder names from Crowdin already match the NVDA directory structure, I've updated This makes the workflow leaner and faster by avoiding unnecessary Python calls. Thanks again for the great suggestion! ☺ |
|
Thanks for the tips, @nvdaes! I'll make sure to add the license header and my name to the scripts to comply with NV Access requirements. Regarding Pyright, I'll do my best to keep the code clean and follow NVDA standards so the PR is as professional as possible. I'm glad to hear we are almost there! ☺ |
|
Regarding the author name and license, I'll add them to be fully compliant with NV Access rules. It's not a priority for me personally, but I want to make sure the PR meets all the formal requirements for a smooth integration. ☺ |
|
Hi @nvdaes, I had a small follow-up regarding the script headers. Would you mind adding the copyright information? I think you're more familiar with that than I am 🙂 Also, for Let me know what you think! |
|
Thanks @abdel792
Also, generally functions are documented as follows, in NVDA:
From markdownTranslate.py:
def getGithubRepoURL() -> str | None:
"""
Get the GitHub repository URL from git remote origin.
return: The raw GitHub URL for the repository, or None if it cannot be
determined.
"""
El mié, 29 abr 2026 a las 11:08, abdel792 ***@***.***>)
escribió:
… *abdel792* left a comment (nvaccess/AddonTemplate#1)
<#1 (comment)>
"Thanks for the tips, @nvdaes <https://github.com/nvdaes>! I'll make sure
to add the license header and my name to the scripts to comply with NV
Access requirements. Regarding Pyright, I'll do my best to keep the code
clean and follow NVDA standards so the PR is as professional as possible.
I'm glad to hear we are almost there! ☺"
—
Reply to this email directly, view it on GitHub
<#1 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADYTVZDOLXPMQZXTRJLEUVD4YHA7ZAVCNFSM6AAAAACNAEHU72VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DGNBSGI4DKNBVG4>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
OK, I'll add copyrights and I'll try to document the functions following
NVDA code standards, in your Python file!
El mié, 29 abr 2026 a las 11:19, abdel792 ***@***.***>)
escribió:
… *abdel792* left a comment (nvaccess/AddonTemplate#1)
<#1 (comment)>
Hi @nvdaes <https://github.com/nvdaes>,
I had a small follow-up regarding the script headers.
Would you mind adding the copyright information? I think you're more
familiar with that than I am 🙂
Also, for crowdinSync.ps1, it would make sense to list both of us as
authors, since you contributed to it as well.
Let me know what you think!
—
Reply to this email directly, view it on GitHub
<#1 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADYTVZEX2CXXEDWFDDZPDQD4YHCLLAVCNFSM6AAAAACNAEHU72VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DGNBSGM2TOOJZGI>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
Thanks @nvdaes, That's very kind of you! You're right, adding the type annotations and docstrings following NVDA standards will definitely help with Pyright and the final review. |
- Renamed functions to camelCase (findFileId, getScoreFromApi) to match NVDA standards[cite: 1]. - Added Python type annotations to improve Pyright compatibility[cite: 1]. - Included detailed docstrings using @param and @type tags for documentation[cite: 1]. - Refined the main function entry point with proper type hinting[cite: 1]. Special thanks to @nvdaes for the guidance on code standards and for offering to handle the license headers!
|
Hi again @nvdaes, I've just pushed the latest updates to
I'll let you add the copyright headers as you kindly offered. I think the script logic and structure are now fully ready for the final review. Thank you again for your precious help! ☺ |
|
Hi @abdel792. I'll add the copyright, seems that this is not added?
Also, in NVDA standards, probably not required here, variables are writen
in camelcase, for example, projectId.
Also, the format for docstrings is:
:param:, we use a colon, not at symbol.
And in NVDA, NV Access is included in the copyright. We can include them
since they are reviewing this and this repo is maintained by them.
I can do all this if you cannot.
And this is ready!
El mié, 29 abr 2026 a las 13:14, abdel792 ***@***.***>)
escribió:
… *abdel792* left a comment (nvaccess/AddonTemplate#1)
<#1 (comment)>
Hi again @nvdaes <https://github.com/nvdaes>,
I've just pushed the latest updates to checkTranslation.py!
- *NVDA Standards*: I have renamed the functions to *camelCase* (
findFileId, getScoreFromApi) to align with NVDA's coding style.
- *Documentation & Types*: I've added *type annotations* to satisfy
Pyright and included *docstrings* using the @param and @type format
for better maintainability[cite: 1].
I'll let you add the copyright headers as you kindly offered. I think the
script logic and structure are now fully ready for the final review. Thank
you again for your precious help! ☺
—
Reply to this email directly, view it on GitHub
<#1 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADYTVZHZ2YSNTOMN25ASAK34YHP25AVCNFSM6AAAAACNAEHU72VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DGNBTGE3TAMJTGQ>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
Hi @nvdaes, Thank you for these final clarifications! Please go ahead and add the copyrights (including NV Access as you suggested) and update the docstrings to the I really appreciate you taking care of these last formal details. Once you're done, I think we're all set for the final integration! ☺ |
|
Hi @nvdaes, Thanks for the updates! I noticed a small bug in the last commit that triggers an API ERROR: 'language_id'. It seems that when converting variables to camelCase, the dictionary key for the Crowdin API response was changed to language_id (line 98). However, the Crowdin API returns this specific key as languageId. I've tested it by changing it back to langApi = item["data"]["languageId"] and it works perfectly again. Do you want me to push the fix or will you handle it? Thanks! ☺ |
The recent refactoring to camelCase accidentally changed the dictionary key 'languageId' to 'language_id' when accessing the API response data. Since the Crowdin API strictly returns 'languageId', this caused a KeyError. - Restored 'languageId' in getScoreFromApi function to ensure proper score retrieval[cite: 2]. - Verified that other internal camelCase variables remain unchanged to satisfy NVDA coding standards[cite: 2].
|
@abdel792 wrote:
Please fix it yourself. Do you want to test it in one of your add-ons, or should I perform the final test? |
|
Hi @nvdaes, I've just pushed a fix for the During the camelCase refactoring, the dictionary key for the Crowdin API response was changed to I have:
The script is now working perfectly again for all file types (.po, .md, and .xliff). Sorry for missing that during the initial review! ☺ |
|
Thanks so much. I'l perform the final test. |
|
Hi @nvdaes, I've just pushed the fix for I am currently testing the script directly on my PC using PowerShell, as I mentioned before. I'm iterating through my local add-on directories and querying the Crowdin API for each one to verify the scores. Everything is working as expected now! You can perform a final test on your side if you wish, but from my end, the script is fully functional and ready for the final review. ☺ |
|
I'll perform a final test to show publicly that it works. Thanks for your work! |
|
@abdel792 , I've run pre-commit to fix chekcs. https://github.com/nvdaes/readFeeds/actions/runs/25120306831/job/73618870755 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Copy-Item "$addonId.xliff" $tempXliff -Force | ||
| Write-Host "DEBUG: Updating XLIFF source based on readme.md..." | ||
| uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile |
There was a problem hiding this comment.
A temp file is created for the previous XLIFF (GetTempFileName()), but it’s never removed. Wrap the update in a try/finally (or remove the temp file after use) to avoid leaking temp files on repeated runs.
| Copy-Item "$addonId.xliff" $tempXliff -Force | |
| Write-Host "DEBUG: Updating XLIFF source based on readme.md..." | |
| uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile | |
| try { | |
| Copy-Item "$addonId.xliff" $tempXliff -Force | |
| Write-Host "DEBUG: Updating XLIFF source based on readme.md..." | |
| uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile | |
| } finally { | |
| if (Test-Path $tempXliff) { | |
| Remove-Item $tempXliff -Force | |
| } | |
| } |
| #### 2. GitHub Secrets | ||
| To allow the workflows to communicate with Crowdin, you must add the following secrets to your GitHub repository (`Settings > Secrets and variables > Actions`): | ||
| * `crowdinAuthToken`: Paste your Crowdin API token here. | ||
|
|
There was a problem hiding this comment.
The documented secret name (crowdinAuthToken) doesn’t match what the workflow actually reads (secrets.CROWDIN_TOKEN). As written, following the README will leave the workflow without credentials. Align the README and workflow on a single secret name (either change the workflow to use secrets.crowdinAuthToken or update the documentation to instruct users to create CROWDIN_TOKEN).
| * **Workflows:** `.github/workflows/crowdinL10n.yml** | ||
| * **Scripts:** The `.github/scripts/` folder containing `checkTranslation.py`, `langCodes.py`, `languageMappings.json`, `setOutputs.py`, and `crowdinSync.ps1`. |
There was a problem hiding this comment.
The workflow bullet has broken Markdown (missing closing backtick / bold marker), and the scripts list references langCodes.py which isn’t present in .github/scripts in this PR (while markdownTranslate.py is present but not mentioned). Update this section so the file list matches what’s actually shipped and renders correctly.
| * **Workflows:** `.github/workflows/crowdinL10n.yml** | |
| * **Scripts:** The `.github/scripts/` folder containing `checkTranslation.py`, `langCodes.py`, `languageMappings.json`, `setOutputs.py`, and `crowdinSync.ps1`. | |
| * **Workflows:** `.github/workflows/crowdinL10n.yml` | |
| * **Scripts:** The `.github/scripts/` folder containing `checkTranslation.py`, `markdownTranslate.py`, `languageMappings.json`, `setOutputs.py`, and `crowdinSync.ps1`. |
| # paths are relative to the configuration file. | ||
| ".github/scripts/markdownTranslate.py", | ||
| ".github/scripts/checkTranslation.py", | ||
| ".github/scripts/langCodes.py", |
There was a problem hiding this comment.
Pyright’s exclude list includes .github/scripts/langCodes.py, but that file doesn’t exist in this PR. Either add the missing script or remove the stale exclude entry to avoid confusion about expected tooling/scripts in the template.
| ".github/scripts/langCodes.py", |
| cancel-in-progress: true | ||
|
|
||
| env: | ||
| crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} |
There was a problem hiding this comment.
The workflow sets crowdinAuthToken from secrets.CROWDIN_TOKEN, but the repository documentation instructs users to create a crowdinAuthToken secret. This mismatch will cause authentication to fail for users following the README. Use a single secret name consistently (e.g., secrets.crowdinAuthToken) and update docs accordingly.
| crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} | |
| crowdinAuthToken: ${{ secrets.crowdinAuthToken }} |
| - name: Get add-on info | ||
| id: getAddonInfo | ||
| shell: pwsh | ||
| run: uv run ./.github/scripts/setOutputs.py |
There was a problem hiding this comment.
uv run ./.github/scripts/setOutputs.py relies on executing a .py file directly. Since setOutputs.py has no shebang and uv run executes commands without a shell, this will typically fail on Windows with “not a valid Win32 application”. Invoke it explicitly via Python (e.g., uv run python ./.github/scripts/setOutputs.py).
| run: uv run ./.github/scripts/setOutputs.py | |
| run: uv run python ./.github/scripts/setOutputs.py |
| $addonId = $env:ADDON_ID.Trim() | ||
| if (-not $addonId) { | ||
| Write-Error "Failed to get addon ID." | ||
| exit 1 | ||
| } |
There was a problem hiding this comment.
$env:ADDON_ID.Trim() will throw if ADDON_ID isn’t set (null), so the script can terminate before reaching the intended error message. Read the env var first and only call .Trim() after verifying it’s non-null/non-empty (or use [string]::IsNullOrWhiteSpace).
| $addonId = $env:ADDON_ID.Trim() | |
| if (-not $addonId) { | |
| Write-Error "Failed to get addon ID." | |
| exit 1 | |
| } | |
| $rawAddonId = $env:ADDON_ID | |
| if ([string]::IsNullOrWhiteSpace($rawAddonId)) { | |
| Write-Error "Failed to get addon ID." | |
| exit 1 | |
| } | |
| $addonId = $rawAddonId.Trim() |
| uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile | ||
| } else { | ||
| Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..." | ||
| uv run .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile |
There was a problem hiding this comment.
uv run .github/scripts/markdownTranslate.py ... attempts to execute the Python file as a program. Since markdownTranslate.py isn’t an executable with a shebang, this is likely to fail under uv run. Call it explicitly via Python (e.g., uv run python .github/scripts/markdownTranslate.py ...) for both the update and generate commands.
| uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile | |
| } else { | |
| Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..." | |
| uv run .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile | |
| uv run python .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile | |
| } else { | |
| Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..." | |
| uv run python .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile |
This pull request introduces a complete, automated workflow for synchronizing translations with Crowdin, including new scripts, a scheduled GitHub Actions workflow, and supporting documentation. The main changes add Python and PowerShell scripts for translation status checking and synchronization, a workflow for scheduled and manual Crowdin sync, language mapping configuration, and documentation updates. These improvements enable seamless translation management for add-on projects.
Crowdin Synchronization Workflow and Automation:
.github/workflows/crowdinL10n.ymlto automate translation synchronization with Crowdin, running weekly and on demand. It sets up the environment, downloads required tools, and triggers the sync process..github/scripts/crowdinSync.ps1, a comprehensive PowerShell script that updates source files, uploads them to Crowdin, exports translations, evaluates translation quality, and commits updates back to the repository.Translation Quality and Language Mapping Utilities:
.github/scripts/checkTranslation.py, a Python script that checks translation progress for a given file and language using the Crowdin API, supporting quality thresholds for importing translations..github/scripts/languageMappings.jsonto map local language codes to Crowdin-compatible codes, ensuring correct language matching during synchronization..github/scripts/setOutputs.py, a Python script to extract and expose the add-on ID for use in workflows.Configuration and Documentation:
pyproject.tomlto include new scripts in the exclude list for packaging and removed the unusedrequestsdependency. [1] [2]readme.mdwith detailed instructions on setting up and using the Crowdin translation workflow, including project setup and required secrets..python-versionspecifying Python 3.13 for consistent workflow environments.These changes collectively provide a robust, maintainable infrastructure for managing translations in add-on projects with Crowdin.